自动加载配置 - 美团组件MtDefaultContextListener分析

探寻一直被忽略的配置加载过程

题图:from Zoommy

1.疑惑

  1. 在通过脚手架搭建的大多数美团项目中,很少见到显式指定配置文件所在目录的。
  2. 一些没有在配置文件中定义的变量,在MCC(web统一配置平台)中配置后,却可以在项目中直接使用。

以上两个问题,其实可以归纳为一个问题——这些项目是如何加载配置文件以及MCC中的配置的?

2.追查

2.1 Web.xml

web.xml,这个最容易被忽视的配置文件,其实是web项目的核心配置文件。

web项目的一切都是从这里开始。

context-param、listener、filter、servlet,一切的一切都在这里定义。因此,想研究web项目的配置加载过程,自然从这里开始。

2.1.1 Web.xml加载顺序

  1. 容器读取节点,并创建一个ServletContext实例,以节点的name作为键,value作为值,存储到上下文环境中。
  2. 容器读取节点,根据配置的class类路径来创建监听。
  3. 容器读取节点,根据配置路径实例化过滤器。
  4. 容器读取节点,初始化servlet。

2.2 Spring配置文件的加载

众所周知,applicationContext.xml是Spring的配置文件,此文件的加载方式是通过context-param和ContextLoaderListener来加载的。

简单来说就是通过将配置文件路径写在contextConfigLocation里,然后让ContextLoaderListener获取并加载配置到上下文中。

示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml,
classpath:database/applicationContext-mybatis-crm-customer.xml,
classpath:database/applicationContext-mybatis-crm-plan.xml,
classpath:database/applicationContext-mybatis-etl.xml,
classpath:database/applicationQuartz.xml,
classpath:applicationContext-thrift.xml,
classpath:thrift-server.xml
</param-value>
</context-param>
 
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

2.3 MCC以及普通配置文件的加载

其实Java EE标准既然已定,那么各个框架、各种服务实现配置文件加载的机制也就大体相同。

因此,参考Spring加载配置的做法,我们可以在web.xml文件中发现另外一个具有美团烙印的Listener——MtDefaultContextListener。

我们只需要跟进他的contextInitialized方法看一下,就可以知道其是如何加载配置的了。

结论先行:
MtDefaultContextListener的主要工作就是将配置放置到ConfigurationManager.instance这个静态变量中。

2.3.1 MtDefaultContextListener的加载过程

通过分析代码,可以发现,MtDefaultContextListener做的工作主要有

  1. 初始化配置,包括系统环境变量、配置文件、MCC配置
  2. 初始化hlb(美团http服务治理组件)

在初始化过程中,我们主要关注initConfigUtilAdapter这个方法。

MtDefaultContextListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void contextInitialized(ServletContextEvent sce) {
init(sce.getServletContext());
}
public void init(ServletContext servletContext) {
ServletContextHolder.set(servletContext);
initDeployContext();
initOtherParams();
// 博主注:此方法为初始化配置的核心方法
initConfigUtilAdapter(getParam(OCTO_APPKEY), getParam(OCTO_ENV, DEFAULT_ENV), getResourcesDirs());
initHlb();
finishInit();
}
2.3.1.1 initConfigUtilAdapter

跟进initConfigUtilAdapter方法,发现配置加载分为三类。

  1. 系统环境变量
  2. 配置文件
  3. MCC配置

initConfigUtilAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void initConfigUtilAdapter(String appkey, String env, String[] resourcesDir) {
// 加载系统变量
if (! Boolean.valueOf(getParam(SKIP_SYSTEM, Boolean.FALSE.toString()))) {
ConfigUtilAdapter.addConfiguration(new SystemConfiguration());
}
// 加载配置文件
if (! Boolean.valueOf(getParam(SKIP_PROPERTIES, Boolean.FALSE.toString()))) {
ConfigUtilAdapter.addConfiguration(new PropertiesConfiguration(resourcesDir));
}
// 加载mcc配置
if (! Boolean.valueOf(getParam(SKIP_MCC, Boolean.FALSE.toString()))) {
ConfigUtilAdapter.addConfiguration(new MccConfiguration(appkey, env, "."));
}
ConfigUtilAdapter.init();
}
2.3.1.2 ConfigUtilAdapter.init()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 初始化配置源,一旦执行,里面的数据源即被绑定,无法更改
*/
public synchronized static void init() {
Preconditions.checkState(! hadInit, StringUtil.format("[{}]只能加载一次!", ConfigUtilAdapter.class.getSimpleName()));
//自带的还有一个ConcurrentMapConfiguration,所以这里只好检查长度是否大于1了
Preconditions.checkState(compositeConfig.getConfigurations().size() > 1,
StringUtil.format("[{}]没有加载任何配置,请至少添加一个配置源", ConfigUtilAdapter.class.getSimpleName()));
if (! ConfigurationManager.isConfigurationInstalled()) {
// 重点方法
ConfigurationManager.install(compositeConfig);
 
Preconditions.checkState(ConfigurationManager.isConfigurationInstalled(), StringUtil.format("[{}]加载失败,请找相关负责人!",
ConfigUtilAdapter.class.getSimpleName()));
}
Iterable<String> configurationNames = Iterables.transform(compositeConfig.getConfigurations(), new Function<AbstractConfiguration, String>() {
@Nullable
@Override
public String apply(AbstractConfiguration input) {
return input.getClass().getSimpleName();
}
});
LOG.info("[{}]加载的具体配置有:{}", ConfigUtilAdapter.class.getSimpleName(), configurationNames);
ConfigurationLog.successInit(ConfigUtilAdapter.class, getAll());
hadInit = true;
}
2.3.1.3 ConfigurationManager.install
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static synchronized void install(AbstractConfiguration config) throws IllegalStateException {
if (!customConfigurationInstalled) {
 
// 重点方法
setDirect(config);
 
if (DynamicPropertyFactory.getBackingConfigurationSource() != config) {
DynamicPropertyFactory.initWithConfigurationSource(config);
}
} else {
throw new IllegalStateException("A non-default configuration is already installed");
}
}
2.3.1.4 setDirect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static synchronized void setDirect(AbstractConfiguration config) {
if (instance != null) {
Collection<ConfigurationListener> listeners = instance.getConfigurationListeners();
// transfer listeners
// transfer properties which are not in conflict with new configuration
for (Iterator<String> i = instance.getKeys(); i.hasNext();) {
String key = i.next();
Object value = instance.getProperty(key);
if (value != null && !config.containsKey(key)) {
config.setProperty(key, value);
}
}
if (listeners != null) {
for (ConfigurationListener listener: listeners) {
if (listener instanceof ExpandedConfigurationListenerAdapter
&& ((ExpandedConfigurationListenerAdapter) listener).getListener()
instanceof DynamicProperty.DynamicPropertyListener) {
// no need to transfer the fast property listener as it should be set later
// with the new configuration
continue;
}
config.addConfigurationListener(listener);
}
}
}
ConfigurationManager.removeDefaultConfiguration();
 
// 重点方法
ConfigurationManager.instance = config;
 
ConfigurationManager.customConfigurationInstalled = true;
ConfigurationManager.registerConfigBean();
}

划重点:静态变量ConfigurationManager.instance在此时已经被赋值为config

2.3.2 ConfigUtilAdapterRegister

Spring提供了一种加载配置文件的机制(applicationContext本身是如何加载的不再赘述),PropertyPlaceholderConfigurer,只需要在Spring配置文件中配置就可以指定配置文件路径加载。

1
2
3
4
5
6
7
8
9
10
// PropertyPlaceholderConfigurer使用方法简单示例
 
<bean id="propertyConfigurer"class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>/WEB-INF/mail.properties</value>
<value>classpath: conf/sqlmap/jdbc.properties</value>
</list>
</property>
</bean>

美团组件通过继承PropertyPlaceholderConfigurer并重写loadProperties方法的方式,将ConfigurationManager.instance中的所有配置加载到Spring容器中。
完成这个工作的就是ConfigUtilAdapterRegister。

2.3.2.1 applicationContext.xml

首先在spring配置文件中加载ConfigUtilAdapterRegister

1
2
3
4
5
6
7
<bean id="configUtilAdapterConfigurer"
class="com.sankuai.meituan.basic.conf.ConfigUtilAdapterRegister">
<property name="ignoreUnresolvablePlaceholders" value="true"/>
<property name="order">
<util:constant static-field="org.springframework.core.Ordered.LOWEST_PRECEDENCE"/>
</property>
</bean>
2.3.2.2 ConfigUtilAdapterRegister

本质就是把ConfigurationManager.instance的所有配置加载到spring中,注意上文已经提到,ConfigurationManager.instance已被MtDefaultContextListener赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 将{@link ConfigUtilAdapter}中的配置全部导到spring里面,然后在spring配置文件里面就可以使用{@link ConfigUtilAdapter}的配置了<br>
* !!!注意:在设置的那一刻就把配置导到spring里,所以在spring配置里无法响应配置的变更<br>
* TODO 检验是否可以使用getDynamicString方式<br>
*/
public class ConfigUtilAdapterRegister extends PropertyPlaceholderConfigurer {
@Override
protected void loadProperties(Properties props) throws IOException {
super.loadProperties(props);
for (String key : Lists.newArrayList(ConfigUtilAdapter.getGolbalConfig().getKeys())) {
props.put(key, ConfigUtilAdapter.getString(key));
}
}
}
2.3.2.3 ConfigUtilAdapter.getGolbalConfig()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static AbstractConfiguration getGolbalConfig() {
checkInit();
return ConfigurationManager.getConfigInstance();
}
 
public static AbstractConfiguration getConfigInstance() {
if (instance == null) {
synchronized (ConfigurationManager.class) {
if (instance == null) {
instance = getConfigInstance(Boolean.getBoolean(DynamicPropertyFactory.DISABLE_DEFAULT_CONFIG));
}
}
}
 
// 返回ConfigurationManager.instance这个静态变量, 已由之前的MtDefaultContextListener加载。
return instance;
}

2.3.3 如何保证MtDefaultContextListener先于Spring加载配置到ConfigurationManager.instance

由web.xml来保证

之前提到过,Spring加载配置文件是由ContextLoaderListener完成的。

web.xml中配置的listener是顺序加载的。因此MtDefaultContextListener需要配置在ContextLoaderListener前面。

如下代码配置。

web.xml

1
2
3
4
5
6
<listener>
<listener-class>com.sankuai.meituan.basic.webapp.MtDefaultContextListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

3.流程草图

4.直接使用Commons Configutation和Archaius的Demo

如上文所述,MtDefaultContextListener加载配置的原理就已经解释清楚了。

但是其中提到的ConfigurationManager是什么一直没有进行说明。其实MtDefaultContextListener是使用了Apache和Netflix开源的框架Commons Configutation和Archaius;而ConfigurationManager就是其提供的配置管理器。

接下来我们就直接使用这两个框架完成一个demo。

4.1初始化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static final PropertiesConfiguration CONFIG = new PropertiesConfiguration();
 
 
CompositeConfiguration compositeConfiguration = new CompositeConfiguration();
// 动态更新
AbstractPollingScheduler scheduler = new FixedDelayPollingScheduler(3000, 1000, true); // or use your own scheduler
PolledConfigurationSource source = new URLConfigurationSource(new File("/Users/cuibaosen/Desktop/testDynamic.properties").toURI().toURL()); // or use your own source
DynamicConfiguration dynamicConfig = new DynamicConfiguration(source, scheduler);
compositeConfiguration.addConfiguration(dynamicConfig);
// 文件配置
PropertiesConfiguration propertiesConfiguration = new PropertiesConfiguration("/Users/cuibaosen/Desktop/testCommon.properties");
compositeConfiguration.addConfiguration(propertiesConfiguration);
// 模拟接收通知的更新配置
DynamicPropertyUpdater updater = new DynamicPropertyUpdater();
updater.updateProperties(WatchedUpdateResult.createFull(new HashMap<>()), CONFIG, false);
compositeConfiguration.addConfiguration(CONFIG);
ConfigurationManager.install(compositeConfiguration);

4.2获取配置

Controller接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 查询配置中的属性值
*
* @param name key值
*/
@RequestMapping("/query")
public String demo(String name) throws MalformedURLException {
return ConfigurationManager.getConfigInstance().getString(name);
}
/**
* 模拟更新MCC配置
*
* @param key
* @param value
*/
@RequestMapping("/updateMcc")
public String update(String key, String value) {
// 新值
HashMap<String, String> newMap = new HashMap<>();
newMap.put(key, value);
// 旧值
HashMap<String, String> oldMap = new HashMap<>();
Iterator<String> it = DemoApplication.CONFIG.getKeys();
while (it.hasNext()) {
String theKey = it.next();
oldMap.put(theKey, DemoApplication.CONFIG.getString(theKey));
}
// 获取新值与旧值的异同,以便获得更新后的值
MapDifference<String, Object> oldAndNewDiff = Maps.difference(oldMap, newMap);
Map<String, Object> changed = Maps.transformValues(oldAndNewDiff.entriesDiffering(), new Function<MapDifference.ValueDifference<Object>, Object>() {
@Nullable
@Override
public Object apply(@Nullable MapDifference.ValueDifference<Object> input) {
return input.rightValue();
}
});
DynamicPropertyUpdater updater = new DynamicPropertyUpdater();
WatchedUpdateResult updateResult = WatchedUpdateResult.createIncremental(oldAndNewDiff.entriesOnlyOnRight(), changed, oldAndNewDiff.entriesOnlyOnLeft());
// 更新配置中的值
updater.updateProperties(updateResult, DemoApplication.CONFIG, false);
return "success";
}

5.结语

关于MtDefaultContextListener的配置加载原理分析到这里就全部结束了。

用一句话总结:

MtDefaultContextListener就是利用Servlet的Listener与Spring的PropertyPlaceholderConfigurer,通过Archaius框架,把系统变量、配置文件、mcc配置在web app启动时加载到spring容器中的。